Topic 3 Multithreading (1)

Leedehai
Friday, April 28, 2017
Monday, May 1, 2017

A thread of execution is the smallest sequence of instructions that can be managed by a scheduler.
A thread is a component of a process, a recipe of independent execution.
Multiple threads can exist within one process, being executed concurrently and sharing the process's resources such as memory space, executable code, variables, etc. while different processes do not share them.
There are a lot of ideas in multiprocessing which lend themselves to multithreading, another form of concurrency programming, where multiple logical control flow overlap or interleave in time.

3.1 Overview

3.1.1 A naïve example

In psychology, introversion is the state of being predominantly interested in one's own mental self. Introverts are typically perceived as more reserved or reflective, but mistaking introversion for shyness is a common error. They can recharge themselves by spending time alone.

Introverts prefer solitary to social activities, but do not necessarily fear social encounters.

#include <stdio.h> #include <pthread.h> /* thread routine: notice the return type and arugument type */ void *recharge(void *unused) { printf("I recharge by spending time alone.\n"); sleep(1); return NULL; /* to statisfy the prototype */ } int main() { pthread_t introverts[6]; for (size_t i = 0; i < 6; i++) /* create 6 independent threads */ /* All args are pointers */ pthread_create(&introverts[i], NULL, recharge, NULL); for (size_t i = 0; i < 6; i++) /* block and wait for the child thread's termination */ /* The first arg is a TID, the second is a pointer to pointer */ pthread_join(introverts[i], NULL); printf("Everyone's recharged!\n"); return 0; } /* takes ~1 sec to run due to multithreading, as opposed to ~6 sec */

3.1.1.1 Explanations

An illustration of creating and joining 2 child threads:

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ start finish a single process ▲──●──────────────────●─ ─ ┐ master thrd. │ cr child thrd. 1 │ jn ───────────────┴───┬──────────────────────▼───▲─────────▶ cr │ child thrd. 2 jn │ ▼──────●────────●─ ─ ─ ─ ─ ┘ start finish └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ cr: pthread_create() jn: pthread_join()

SIDE NOTE:
ANSI C doesn't provide native support for threads. But POSIX threads (aka pthread) comes with all standard UNIX and Linux installations of gcc, as concurrency programming requires the cooperation of the kernel.

3.1.1.2 The CPU and memory, thread context switch

In the same process, a thread's access to another thread's objects in stack is provided by pointers/references. SIDE NOTE:
Preemptive multitasking: the scheduler can switch the task (process/thread) out from CPU and switch another in, at any time it feels like.
Cooperative multitasking: the task (process/thread) can only be switched out from CPU when it allows - say, a stage completes - and other tasks have to wait.
Cooperative multitasking was the primary scheduling scheme for 16-bit applications employed by Microsoft Windows before Windows 95, and by the classic Mac OS.
In computer network protocols, similar mechanisms is applied.

3.2 Scheduler's role in thread (un)safety

scenaio 1: ┌────────┐┌────────┐ ──▶ time │write a:││read a, │ thread 1 │ a = 1 ││& print │ => print 1 └────────┘└────────┘ ┌────────┐┌────────┐ thread 2 │write a:││read a, │ => print 2 │ a = 2 ││& print │ └────────┘└────────┘ OK ─────────────────────────────────────────────────────────────────────── scenaio 2: ┌────────┐ ┌────────┐ ERRONEOUS │write a:│ │read a, │ thread 1 │ a = 1 │ │& print │ => print 2 └────────┘ └────────┘ ┌────────┐ ┌────────┐ thread 2 │write a:│ │read a, │ => print 2 │ a = 2 │ │& print │ └────────┘ └────────┘

CS145, huh? "Two actions conflict if they are part of different transactions, involve the same variable, and at least one of them is a write." There is a WW conflict between thread 1 & 2, and in scenario 2, that conflict caused a problem. The second scenario is a bad schedule.

3.2.1 An example of problematic multithreading

Suppose there are 6 extroverts and 1 tag-along introvert in an array. The extroverts need to print their greetings but the introvert needs to remain silent.

/* Problematic multithreading */ #include ... static const char *const names[] = { "Albert Chon", "John Carlo Buenaflor", "Jessica Guo", "Lucas Ege", "Sona Allahverdiyeva", "Yun Zhang", "Tagalong Introvert Jerry Cain" }; void *recharge(void *arg) { printf("I'm %s. Empowered to meet you.\n", names[*(size_t *)arg]); /* type cast, don't forget */ return NULL; } int main() { pthread_t extroverts[6]; for (size_t i = 0; i < 6; i++) { pthread_create(&extroverts[i], NULL, recharge, &i); } for (size_t i = 0; i < 6; i++) { pthread_join(extroverts[i], NULL); } printf("Everyone's recharged!\n"); return 0; }

In this program, the array names has 7 pointers to C-strings, and the program should have print the first 6 strings one time each (though not necessarily in order). But the actual output is not deterministic: some strings get printed more than once, some strings is not printed, and sometimes the last string, which should not have been printed, gets printed:

$ ./confusing-extroverts I'm John Carlo Buenaflor. Empowered to meet you. I'm Jessica Guo. Empowered to meet you. I'm Sona Allahverdiyeva. Empowered to meet you. I'm Sona Allahverdiyeva. Empowered to meet you. I'm Yun Zhang. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. Everyone's recharged! $ ./confused-extroverts I'm Sona Allahverdiyeva. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. Everyone's recharged! $ ./confused-extroverts I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. I'm Tagalong Introvert Jerry Cain. Empowered to meet you. Everyone's recharged!

The bug is:

Q: can "Albert Chon" be printed more than once?
A: No. As the first string, "Albert Chon" can be printed either 0 or 1 time, but not more than that.

Q: how many times can the k-th string (k = 0, 1, ..., 6) be printed?
A: the k-th string can be printed 0 ~ (k + 1) times.

Illustration:

Scenario 1: OK get 5 ch. 5 ○─●──────────... t ○ get &i get 4 o ● dereference ch. 4 ○──●────────────────... get 3 b ch. 3 ○───●────────────────────... e get 2 ch. 2 ○───●────────────────────────... j get 1 o ch. 1 ○───●──────────────────────────────... i get 0 n ch.0 ○──●─────────────────────────────────... e d ───●──────●───────●───────●───────●───────●──────●───────......─────────▶ i = 0 1 2 3 4 5 6(break) master thread
Scenario 2: ERRONEOUS get 5 ch. 5 ○─●──────────... t ○ get &i get 4 o ● dereference ch. 4 ○─●────────────────... get 4 b ch. 3 ○────────────●────────────... e get 5 ch. 2 ○────────────────────────●───────... j get 1 o ch. 1 ○───●──────────────────────────────... i get 1 n ch. 0 ○──────●──────────────────────────... e d ───●───●───────────●───────●───────●──────●──────●───────......────────▶ i = 0 1 2 3 4 5 6(break) master thread
Scenario 3: ERRONEOUS get 6 ch. 5 ○──────────●───... t ○ get &i get 6 o ● dereference ch. 4 ○───────────────●────... get 6 b ch. 3 ○──────────────────────●────... e get 6 ch. 2 ○───────────────●──────... j get 6 o ch. 1 ○──────────────────────────────────────●──... i get 6 n ch. 0 ○───────────────────────────●──────... e d ───●───●───●───●───●───●───●─────────────────────......────────────────▶ i = 0 1 2 3 4 5 6(break) master thread

To fix this, you can pass in the address to the C-strings themselves, instead of the address of the index i.

/* Bug fixed */ #include ... static const char *const names[] = { "Albert Chon", "John Carlo Buenaflor", "Jessica Guo", "Lucas Ege", "Sona Allahverdiyeva", "Yun Zhang", "Tagalong Introvert Jerry Cain" }; void *recharge(void *arg) { printf("I'm %s. Empowered to meet you.\n", (const char *)arg); /* type cast, don't forget */ return NULL; } int main() { pthread_t extroverts[6]; for (size_t i = 0; i < 6; i++) { pthread_create(&extroverts[i], NULL, recharge, (void *)names[i]); } for (size_t i = 0; i < 6; i++) { pthread_join(extroverts[i], NULL); } printf("Everyone's recharged!\n"); return 0; }

Putting pthread_join() in the same loop body as the pthread_create() also fixes this bug, but this approach essentially does not utilize the power of multithreading - it is just a sequential program in disguise.

EOF